/*
 * SonarQube Scanner for Maven
 * Copyright (C) 2009-2017 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.apache.maven.shared.dependency.tree;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Stack;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ResolutionListener;
import org.apache.maven.artifact.resolver.ResolutionListenerForDepMgmt;
import org.apache.maven.artifact.versioning.VersionRange;
import org.codehaus.plexus.logging.Logger;

/**
 * Patched version of DependencyTreeResolutionListener to fix MSONAR-135
 */
public class DependencyTreeResolutionListener
  implements ResolutionListener, ResolutionListenerForDepMgmt {
  // fields -----------------------------------------------------------------

  /**
   * The log to write debug messages to.
   */
  private final Logger logger;

  /**
   * The parent dependency nodes of the current dependency node.
   */
  private final Stack<DependencyNode> parentNodes;

  /**
   * A map of dependency nodes by their attached artifact.
   */
  private final Map<Artifact, DependencyNode> nodesByArtifact;

  /**
   * The root dependency node of the computed dependency tree.
   */
  private DependencyNode rootNode;

  /**
   * The dependency node currently being processed by this listener.
   */
  private DependencyNode currentNode;

  /**
   * Map &lt; String replacementId, String premanaged version >
   */
  private Map<String, String> managedVersions = new HashMap<String, String>();

  /**
   * Map &lt; String replacementId, String premanaged scope >
   */
  private Map<String, String> managedScopes = new HashMap<String, String>();

  // constructors -----------------------------------------------------------

  /**
   * Creates a new dependency tree resolution listener that writes to the specified log.
   * 
   * @param logger the log to write debug messages to
   */
  public DependencyTreeResolutionListener(Logger logger) {
    this.logger = logger;

    parentNodes = new Stack<DependencyNode>();
    nodesByArtifact = new IdentityHashMap<Artifact, DependencyNode>();
    rootNode = null;
    currentNode = null;
  }

  // ResolutionListener methods ---------------------------------------------

  /**
   * {@inheritDoc}
   */
  public void testArtifact(Artifact artifact) {
    log("testArtifact: artifact=" + artifact);
  }

  /**
   * {@inheritDoc}
   */
  public void startProcessChildren(Artifact artifact) {
    log("startProcessChildren: artifact=" + artifact);

    if (!currentNode.getArtifact().equals(artifact)) {
      throw new IllegalStateException("Artifact was expected to be " + currentNode.getArtifact() + " but was "
        + artifact);
    }

    parentNodes.push(currentNode);
  }

  /**
   * {@inheritDoc}
   */
  public void endProcessChildren(Artifact artifact) {
    DependencyNode node = parentNodes.pop();

    log("endProcessChildren: artifact=" + artifact);

    if (node == null) {
      throw new IllegalStateException("Parent dependency node was null");
    }

    if (!node.getArtifact().equals(artifact)) {
      throw new IllegalStateException("Parent dependency node artifact was expected to be " + node.getArtifact()
        + " but was " + artifact);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void includeArtifact(Artifact artifact) {
    log("includeArtifact: artifact=" + artifact);

    DependencyNode existingNode = getNode(artifact);

    /*
     * Ignore duplicate includeArtifact calls since omitForNearer can be called prior to includeArtifact on the same
     * artifact, and we don't wish to include it twice.
     */
    if (existingNode == null && isCurrentNodeIncluded()) {
      DependencyNode node = addNode(artifact);

      /*
       * Add the dependency management information cached in any prior manageArtifact calls, since includeArtifact
       * is always called after manageArtifact.
       */
      flushDependencyManagement(node);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void omitForNearer(Artifact omitted, Artifact kept) {
    log("omitForNearer: omitted=" + omitted + " kept=" + kept);

    if (!omitted.getDependencyConflictId().equals(kept.getDependencyConflictId())) {
      throw new IllegalArgumentException("Omitted artifact dependency conflict id "
        + omitted.getDependencyConflictId() + " differs from kept artifact dependency conflict id "
        + kept.getDependencyConflictId());
    }

    if (isCurrentNodeIncluded()) {
      DependencyNode omittedNode = getNode(omitted);

      if (omittedNode != null) {
        removeNode(omitted);
      } else {
        omittedNode = createNode(omitted);

        currentNode = omittedNode;
      }

      omittedNode.omitForConflict(kept);

      /*
       * Add the dependency management information cached in any prior manageArtifact calls, since omitForNearer
       * is always called after manageArtifact.
       */
      flushDependencyManagement(omittedNode);

      DependencyNode keptNode = getNode(kept);

      if (keptNode == null) {
        addNode(kept);
      }
    }
  }

  /**
   * {@inheritDoc}
   */
  public void updateScope(Artifact artifact, String scope) {
    log("updateScope: artifact=" + artifact + ", scope=" + scope);

    DependencyNode node = getNode(artifact);

    if (node == null) {
      // updateScope events can be received prior to includeArtifact events
      node = addNode(artifact);
    }

    node.setOriginalScope(artifact.getScope());
  }

  /**
   * {@inheritDoc}
   */
  public void manageArtifact(Artifact artifact, Artifact replacement) {
    // TODO: remove when ResolutionListenerForDepMgmt merged into ResolutionListener

    log("manageArtifact: artifact=" + artifact + ", replacement=" + replacement);

    if (replacement.getVersion() != null) {
      manageArtifactVersion(artifact, replacement);
    }

    if (replacement.getScope() != null) {
      manageArtifactScope(artifact, replacement);
    }
  }

  /**
   * {@inheritDoc}
   */
  public void omitForCycle(Artifact artifact) {
    log("omitForCycle: artifact=" + artifact);

    if (isCurrentNodeIncluded()) {
      DependencyNode node = createNode(artifact);

      node.omitForCycle();
    }
  }

  /**
   * {@inheritDoc}
   */
  public void updateScopeCurrentPom(Artifact artifact, String scopeIgnored) {
    log("updateScopeCurrentPom: artifact=" + artifact + ", scopeIgnored=" + scopeIgnored);

    DependencyNode node = getNode(artifact);

    if (node == null) {
      // updateScopeCurrentPom events can be received prior to includeArtifact events
      node = addNode(artifact);
      // TODO remove the node that tried to impose its scope and add some info
    }

    node.setFailedUpdateScope(scopeIgnored);
  }

  /**
   * {@inheritDoc}
   */
  public void selectVersionFromRange(Artifact artifact) {
    log("selectVersionFromRange: artifact=" + artifact);

    DependencyNode node = getNode(artifact);

    /*
     * selectVersionFromRange is called before includeArtifact
     */
    if (node == null && isCurrentNodeIncluded()) {
      node = addNode(artifact);
    }

    node.setVersionSelectedFromRange(artifact.getVersionRange());
    node.setAvailableVersions(artifact.getAvailableVersions());
  }

  /**
   * {@inheritDoc}
   */
  public void restrictRange(Artifact artifact, Artifact replacement, VersionRange versionRange) {
    log("restrictRange: artifact=" + artifact + ", replacement=" + replacement
      + ", versionRange=" + versionRange);

    // TODO: track range restriction in node (MNG-3093)
  }

  // ResolutionListenerForDepMgmt methods -----------------------------------

  /**
   * {@inheritDoc}
   */
  public void manageArtifactVersion(Artifact artifact, Artifact replacement) {
    log("manageArtifactVersion: artifact=" + artifact + ", replacement=" + replacement);

    /*
     * DefaultArtifactCollector calls manageArtifact twice: first with the change; then subsequently with no change.
     * We ignore the second call when the versions are equal.
     */
    if (isCurrentNodeIncluded() && !replacement.getVersion().equals(artifact.getVersion())) {
      /*
       * Cache management information and apply in includeArtifact, since DefaultArtifactCollector mutates the
       * artifact and then calls includeArtifact after manageArtifact.
       */
      managedVersions.put(replacement.getId(), artifact.getVersion());
    }
  }

  /**
   * {@inheritDoc}
   */
  public void manageArtifactScope(Artifact artifact, Artifact replacement) {
    log("manageArtifactScope: artifact=" + artifact + ", replacement=" + replacement);

    /*
     * DefaultArtifactCollector calls manageArtifact twice: first with the change; then subsequently with no change.
     * We ignore the second call when the scopes are equal.
     */
    if (isCurrentNodeIncluded() && !replacement.getScope().equals(artifact.getScope())) {
      /*
       * Cache management information and apply in includeArtifact, since DefaultArtifactCollector mutates the
       * artifact and then calls includeArtifact after manageArtifact.
       */
      managedScopes.put(replacement.getId(), artifact.getScope());
    }
  }

  public void manageArtifactSystemPath(Artifact artifact, Artifact replacement) {
    log("manageArtifactScope: artifact=" + artifact + ", replacement=" + replacement);
  }

  // public methods ---------------------------------------------------------

  /**
   * Gets a list of all dependency nodes in the computed dependency tree.
   * 
   * @return a list of dependency nodes
   * @deprecated As of 1.1, use a
   *             {@link org.apache.maven.shared.dependency.tree.traversal.CollectingDependencyNodeVisitor} on the root
   *             dependency node
   */
  public Collection<DependencyNode> getNodes() {
    return Collections.unmodifiableCollection(nodesByArtifact.values());
  }

  /**
   * Gets the root dependency node of the computed dependency tree.
   * 
   * @return the root node
   */
  public DependencyNode getRootNode() {
    return rootNode;
  }

  // private methods --------------------------------------------------------

  /**
   * Writes the specified message to the log at debug level with indentation for the current node's depth.
   * 
   * @param message the message to write to the log
   */
  private void log(String message) {
    int depth = parentNodes.size();

    StringBuffer buffer = new StringBuffer();

    for (int i = 0; i < depth; i++) {
      buffer.append("  ");
    }

    buffer.append(message);

    logger.debug(buffer.toString());
  }

  /**
   * Creates a new dependency node for the specified artifact and appends it to the current parent dependency node.
   * 
   * @param artifact the attached artifact for the new dependency node
   * @return the new dependency node
   */
  private DependencyNode createNode(Artifact artifact) {
    DependencyNode node = new DependencyNode(artifact);

    if (!parentNodes.isEmpty()) {
      DependencyNode parent = parentNodes.peek();

      parent.addChild(node);
    }

    return node;
  }

  /**
   * Creates a new dependency node for the specified artifact, appends it to the current parent dependency node and
   * puts it into the dependency node cache.
   * 
   * @param artifact the attached artifact for the new dependency node
   * @return the new dependency node
   */
  // package protected for unit test
  DependencyNode addNode(Artifact artifact) {
    DependencyNode node = createNode(artifact);

    DependencyNode previousNode = nodesByArtifact.put(node.getArtifact(), node);

    if (previousNode != null) {
      throw new IllegalStateException("Duplicate node registered for artifact: " + node.getArtifact());
    }

    if (rootNode == null) {
      rootNode = node;
    }

    currentNode = node;

    return node;
  }

  /**
   * Gets the dependency node for the specified artifact from the dependency node cache.
   * 
   * @param artifact the artifact to find the dependency node for
   * @return the dependency node, or <code>null</code> if the specified artifact has no corresponding dependency node
   */
  private DependencyNode getNode(Artifact artifact) {
    return nodesByArtifact.get(artifact);
  }

  /**
   * Removes the dependency node for the specified artifact from the dependency node cache.
   * 
   * @param artifact the artifact to remove the dependency node for
   */
  private void removeNode(Artifact artifact) {
    DependencyNode node = nodesByArtifact.remove(artifact);

    if (!artifact.equals(node.getArtifact())) {
      throw new IllegalStateException("Removed dependency node artifact was expected to be " + artifact
        + " but was " + node.getArtifact());
    }
  }

  /**
   * Gets whether the all the ancestors of the dependency node currently being processed by this listener have an
   * included state.
   * 
   * @return <code>true</code> if all the ancestors of the current dependency node have a state of
   *         <code>INCLUDED</code>
   */
  private boolean isCurrentNodeIncluded() {
    for (DependencyNode node : parentNodes) {
      if (node.getState() != DependencyNode.INCLUDED) {
        return false;
      }
    }

    return true;
  }

  /**
   * Updates the specified node with any dependency management information cached in prior <code>manageArtifact</code>
   * calls.
   * 
   * @param node the node to update
   */
  private void flushDependencyManagement(DependencyNode node) {
    Artifact artifact = node.getArtifact();
    String premanagedVersion = managedVersions.get(artifact.getId());
    String premanagedScope = managedScopes.get(artifact.getId());

    if (premanagedVersion != null || premanagedScope != null) {
      if (premanagedVersion != null) {
        node.setPremanagedVersion(premanagedVersion);
      }

      if (premanagedScope != null) {
        node.setPremanagedScope(premanagedScope);
      }

      premanagedVersion = null;
      premanagedScope = null;
    }
  }
}
